package com.hup.vd.service;

import com.google.common.util.concurrent.RateLimiter;
import com.hup.vd.model.config.AppConfig;
import com.hup.vd.model.config.DownloadConfig;
import com.hup.vd.service.netSpeed.NetSpeedCounter;
import com.hup.vd.service.netSpeed.SlidingWindowRateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Objects;
import java.util.function.Consumer;

/**
 * @author hugan
 * @date 2022/8/12
 */
@SuppressWarnings("UnstableApiUsage")
@Slf4j
@Component
public class FileDownloadService {

    /**
     * 读取inputStream的缓存大小,4KB
     */
    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

    private final AppConfig appConfig;
    /**
     * 限速器,二选一
     */
    private final RateLimiter rateLimiter;
    private final SlidingWindowRateLimiter slidingWindow;
    /**
     * {@link DownloadConfig#downloadRateLimit}
     */
    private Integer rateLimit;

    public FileDownloadService(AppConfig appConfig) {
        this.appConfig = appConfig;
        rateLimiter = RateLimiter.create(1);
        slidingWindow = new SlidingWindowRateLimiter();
        notifyConfigUpdated();
    }

    public void notifyConfigUpdated() {
        Integer rateLimitKB = appConfig.getDownloadConfig().getDownloadRateLimit();
        if (Objects.equals(this.rateLimit, rateLimitKB)) return;
        log.info("更新限速={} KB/s", rateLimitKB);
        if (rateLimitKB != null && rateLimitKB * 1024 < DEFAULT_BUFFER_SIZE) {
            throw new IllegalArgumentException("限速不能小于4KB: " + rateLimitKB);
        }
        this.rateLimit = rateLimitKB;
        //实际的"限速次数",一次是[4K, buff的大小]
        Integer realRateCountLimit = rateLimitKB == null ? null : rateLimitKB / 4;
        if (realRateCountLimit == null) {
            rateLimiter.setRate(1);
            slidingWindow.setRateLimit(1);
        } else {
            rateLimiter.setRate(realRateCountLimit);
            slidingWindow.setRateLimit(realRateCountLimit);
        }
    }

    /**
     * 用于断点调试
     */
    @SuppressWarnings({"unused", "FieldCanBeLocal"})
    private static double debugValue;

    /**
     * 实现下载限速
     */
    private void handleRateLimit() {
        /*
        https://blog.csdn.net/qq_39470742/article/details/122226736
        超过阈值时,acquire会阻塞线程
        实测效果
           .两种限速器的实际效果基本一样
           .[windows界面,路由器]上显示,山峰形式的,非平滑流量,断断续续的原因:
                .小阈值时,流量确实是在"窗口期的开始阶段"就被用完了,所以剩余窗口期就是零流量的
                .阈值大于2M/s时,系统界面显示的流量图会变平滑
                .不会出现'超过限制值'的情况(或极少)
         */
        if (rateLimit != null) {
            //限速器,二选一
            rateLimiter.acquire(1);
            //slidingWindow.acquire();
        }
    }

    /**
     * 下载文件:
     * .可中途取消
     * .可限速
     * .改自 {@link FileUtils#copyURLToFile(URL, File, int, int)}
     *
     * @apiNote 注意, 每次返回时, 必须判断 cancelState.isCanceled(), 任务取消时,下载的内容是残缺的
     */
    public void copyURLToFile(final URL url, final File destination,
            final int connectionTimeout, final int readTimeout,
            Consumer<URLConnection> headersSetter,
            CancelState cancelState, NetSpeedCounter speedCounter) throws IOException {
        URLConnection connection = url.openConnection();
        connection.setConnectTimeout(connectionTimeout);
        connection.setReadTimeout(readTimeout);
        if (headersSetter != null) headersSetter.accept(connection);

        InputStream ips = connection.getInputStream();
        FileOutputStream ops = FileUtils.openOutputStream(destination);//会覆盖已有文件
        try {
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int readCount;
            while (-1 != (readCount = ips.read(buffer))) {
                ops.write(buffer, 0, readCount);
                speedCounter.add(4);//可以忽略实际的readCount
                handleRateLimit();
                if (cancelState.isCanceled()) return;
            }
        } finally {
            IOUtils.closeQuietly(ips);
            IOUtils.closeQuietly(ops);
        }
    }

}
